package com.tnovoselec.android.common.async.runner;

import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;

import com.tnovoselec.android.common.async.listener.ResultListener;
import com.tnovoselec.android.common.async.notif.Notifier;

public class ExecutorRunner implements Runner {

	/** The executor we use to run tasks. */
	private final Executor executor;
	/** The notifier we use to notify {@link ResultListener result listeners}. */
	private final Notifier notifier;

	/**
	 * Constructs a {@code Runner} with the specified executor to run the async
	 * tasks and the specified notifier to handle notifying listeners.
	 *  
	 * @param executor An executor which takes care of scheduling tasks.
	 * @param notifier 
	 *        A notifier which is responsible for notifying
	 *        {@link ResultListener result listeners} of tasks having completed.
	 */
	public ExecutorRunner(Executor executor, Notifier notifier) {
		if (executor == null) {
			throw new NullPointerException("Executor is null!");
		} else if (notifier == null) {
			throw new NullPointerException("Notifier is null!");
		}

		this.executor = executor;
		this.notifier = notifier;
	}

	@Override
	public <K, T> Future<T> doAsync(K key, Callable<T> future, ResultListener<K, T> listener) {
		if (future == null) {
			throw new NullPointerException("Future is null!");
		} else if (listener == null) {
			throw new NullPointerException("Listener is null!");
		}

		return doAsync(key, new FutureTask<T>(future), listener);
	}

	@Override
	public <K, T, F extends Future<T> & Runnable> F doAsync(K key, F future, ResultListener<K, T> listener) {
		if (future == null) {
			throw new NullPointerException("Future is null!");
		} else if (listener == null) {
			throw new NullPointerException("Listener is null!");
		}

		// Create a manager which runs the future task, waits for it to end then notifies the listener via the notifier.
		TaskManager<K, T, F> manager = new TaskManager<K, T, F>(key, future, listener, notifier);

		// Enqueue the manager for our executor to run at some point in the future.
		executor.execute(manager);

		// Return the newly created task so that the client has a way of canceling the computation easily.
		return future;
	}

	/**
	 * Responsible for running a future, waiting for it to complete, then  notifying the listener.
	 */
	private static final class TaskManager<K, T, F extends Future<T> & Runnable> implements Runnable {

		/** A runnable future encapsulating the task we're executing. */
		private final F task;

		/** Some arbitrary object which identifies our task to the result listener. We don't care about it at all. */
		private final K key;

		/** The listener who wants to know when the task is done. */
		private final ResultListener<K, T> resultListener;

		/** A notifier who we can get to inform the listener for us. */
		private final Notifier notifier;

		/** Whether this task has already been started. Used to prevent us being run more than once. */
		private final AtomicBoolean hasBeenRun;

		/**
		 * Constructs a TaskManager which when {@link #run} executes the
		 * specified task, notifying the listener upon completion.
		 */
		public TaskManager(K key, F task, ResultListener<K, T> listener, Notifier notifier) {
			if (task == null) {
				throw new NullPointerException("Task is null!");
			} else if (notifier == null) {
				throw new NullPointerException("Notifier is null!");
			} else if (listener == null) {
				throw new NullPointerException("Listener is null!");
			}

			this.task = task;
			this.key = key;
			this.resultListener = listener;
			this.notifier = notifier;
			hasBeenRun = new AtomicBoolean(false);
		}

		@Override
		public void run() {
			// Set the hasBeenRun flag to true indicating that we're running.
			// If the old value was true then we've already set it to true, so
			// someone must have called run() again!
			if (hasBeenRun.getAndSet(true)) {
				throw new IllegalStateException("This TaskManager has already been runned.");
			}

			task.run();
			notifier.notify(resultListener, key, task);
		}
	}
}
